1장. 랭체인이라는 레고 상자
출처: 『RAG 마스터: 랭체인으로 완성하는 LLM 서비스』(브라이스 유·조경아·박수진·김재웅 지음, 프리렉 2025) | 공식: www.langchain.com
코드는 분위기만 —
pip install·|·invoke같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.
이 장은 0장에서 배운 부품(LLM·검색기·임베딩)을 손으로 끼워 맞추게 해 주는 도구, 랭체인을 본다.
레고 상자를 처음 열듯, 어떤 블록이 들어 있고 그 블록을 어떻게 잇는지 천천히 살핀다.
0. 이 장의 새 단어 (0장에 없는 것만 3개)
0장 용어집에 있는 말(LLM·RAG·임베딩·체인·프롬프트·토큰·온도 등)은 다 안다고 보고 간다.
이 장에서 처음 나오는 말은 딱 3개다. 먼저 풀어 둔다.
러너블(runnable)
한 문장 뜻 — 랭체인에서 실행할 수 있는 작업 한 칸. 프롬프트도, 모델도, 파서도 전부 이 한 칸으로 똑같이 생겼다.
일상비유 — 같은 규격의 레고 블록. 바퀴든 문이든 밑면 돌기가 같으니, 무엇이든 끼우면 붙는다.
한 줄 예 —
# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 프롬프트도 모델도 파서도 다 "러너블" → 같은 invoke로 부른다
prompt.invoke({"topic": "더블딥"}) # 프롬프트 한 칸 실행
model.invoke("안녕") # 모델 한 칸 실행
출력 파서(output parser)
한 문장 뜻 — 모델이 뱉은 줄글을 프로그램이 쓰기 좋은 정리된 모양(JSON·표 등)으로 바꿔 주는 부품.
일상비유 — 손님이 말로 늘어놓은 주문을 주방용 주문표 칸에 또박또박 옮겨 적는 점원. 말은 자유롭지만, 표는 칸이 정해져 있다.
한 줄 예 —
# 모델의 줄글 답을 그냥 문자열로 깔끔히 받는다
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = StrOutputParser()
메모리(memory, 대화 기록)
한 문장 뜻 — 챗봇이 방금 전 대화를 기억하게 해, 앞 내용을 이어 말하게 하는 장치.
일상비유 — 통화 중 메모지. "아까 뭐라고 했죠?" 물으면 메모지를 보고 답한다. 메모지가 없으면 매번 처음 본 사람처럼 군다.
한 줄 예 —
# 앞 대화를 적어 두고 다음 질문에 같이 넘긴다
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
history.add_user_message("저축 어떻게 늘려요?")
(귀납 도입) OpenAI만 쓰다가 모델을 바꾸려니 코드 절반이 터졌다
처음엔 OpenAI 한 줄로 충분했다.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
import openai
# 답변을 생성할 LLM 클라이언트나 모델 설정을 준비합니다.
client = openai.OpenAI()
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
client.chat.completions.create(
# 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
model="gpt-4o-mini",
# 모델에게 보낼 대화 메시지 목록을 지정합니다.
messages=[{"role": "user", "content": "안녕하세요!"}]
)
잘 돌아간다. 그래서 회사 곳곳에 이 호출을 흩뿌렸다.
그런데 어느 날 "Gemini가 더 싸다, 갈아타자" 하는 말이 나왔다.
문제가 터진다. OpenAI 호출법과 Gemini 호출법은 생김새가 다르다. 흩뿌려 둔 그 많은 호출을 하나하나 뜯어고쳐야 한다.
모델 하나 바꾸려다 코드 절반을 다시 짠다.
이 고생을 줄이려고 나온 게 랭체인이다.
랭체인은 모델을 표준 규격으로 한 번 감싼다. 그러면 모델 교체가 블록 하나 갈아 끼우는 일로 줄어든다.
# 랭체인 — 모델은 클래스 한 줄. 교체도 한 줄.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import ChatOpenAI
# 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
model = ChatOpenAI(model="gpt-4o")
# 다른 모델로? 윗줄만 ChatMistralAI(...) 로 바꾸면 끝.
이 장은 그 "감싸고 끼우는" 방식을 처음부터 본다.
이 장에서 딱 6가지만
- 랭체인은 한 덩어리가 아니라 부품 상자(생태계)다. core·langchain·community·파트너·langgraph 등 역할별 블록이 들어 있다.
- 랭체인은 모델을 직접 만들지 않는다. OpenAI·Gemini 같은 남의 모델을 표준 규격으로 감싸, 같은 방식으로 부르게 해 준다.
- LCEL은 블록을
|(파이프)로 잇는 방식이다. 프롬프트 → 모델 → 파서를 한 줄로 줄 세운다.- 프롬프트 템플릿은 빈칸 양식이다. 빈칸에 값만 갈아 끼워 모델에게 지시를 만든다. 예시를 몇 개 보여 주는 게 퓨샷.
- 출력 파서는 모델의 줄글을 정리된 모양으로 바꾼다. JSON·검증된 데이터 등.
- 메모리는 챗봇이 앞 대화를 기억하게 한다. 그냥 넘기기 → 차곡차곡 저장 → 자동 관리 → 길면 줄이거나 요약.
여섯 블록을 하나씩 본다. 막히면 0장 용어집으로 돌아가면 된다.
개념 1 — 랭체인은 부품 상자(생태계)다
망가지는 장면
"랭체인 깔았으니 다 되겠지" 하고 한 패키지만 믿었다.
그런데 어떤 기능은 다른 패키지에 있고, 어떤 건 선택 설치다.
뭐가 어디 있는지 모르면, 필요한 블록을 못 찾아 헤맨다.
일상비유
랭체인은 부품이 칸칸이 나뉜 공구 상자다.
드라이버 칸, 나사 칸, 특수 공구 칸이 따로 있다. 칸 이름만 알면 필요한 걸 바로 꺼낸다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 바닥에 깔리는 기본 받침 | import langchain_core |
없으면 위 블록들이 못 선다 |
| 그 위에 얹는 본체 | import langchain |
깔면 core가 자동으로 같이 깔림 |
| 선택 추가 통합 모음 | import langchain_community |
안 깔면 일부 외부 연결 불가 |
한 문장 정의 — 랭체인은 하나의 패키지가 아니라, 역할별로 나뉜 여러 패키지를 조합해 쓰는 생태계다.
주요 블록(패키지)을 한눈에
| 블록 이름 | 무슨 칸인가 |
|---|---|
langchain-core |
상자 바닥. 모델·검색기·파서의 기본 규격과 LCEL을 정한다. 가볍다. |
langchain |
본체. 체인·에이전트 같은 앱 뼈대를 만든다. 깔면 core가 함께 깔린다. |
langchain-community |
커뮤니티가 챙기는 외부 연결 모음. 필요할 때만 선택 설치. |
| 파트너 패키지 | 자주 쓰는 연결만 따로 뺀 것. langchain-openai·langchain-anthropic 등. |
langgraph |
갈림길·되돌이가 있는 복잡한 흐름을 그래프로 그릴 때. |
langserve |
만든 체인을 웹 API로 내보낼 때. |
langsmith |
잘 도는지 들여다보고 점검할 때(디버깅·모니터링). |
예시 폭격 — 의존성 화살표 읽기
(worked) 화살표 "A → B"는 "A가 B에 기댄다"는 뜻이다.
langchain → langchain-core 이므로, langchain만 깔아도 core가 딸려 온다.
# langchain만 깔아도 core가 함께 깔린다
# (직접 의존성: langchain이 core에 직접 기댐)
# 프롬프트·모델·출력 처리를 하나의 실행 흐름으로 이어 붙입니다.
!pip install langchain
import langchain_core # 따로 안 깔았는데도 쓸 수 있음
(부분 완성) langgraph는 core를 피어 의존성으로 쓴다. 즉 "있으면 골라 쓰는" 사이다.
빈칸을 채워 보자. langgraph는 core를 ____(필수로 / 골라서) 쓴다. → 답: 골라서.
(독립 적용) 친구가 "langchain-anthropic은 뭐예요?" 물으면?
→ "자주 쓰는 외부 연결(앤트로픽 Claude)만 따로 뺀 파트너 패키지예요. 필요할 때만 깔면 됩니다."
버전 이야기 (1.2)
랭체인은 버전이 자주 오른다. 큰 줄기만 잡으면 된다.
- 0.1 — 패키지를 잘게 나눔(분리 시작).
- 0.2 — community 의존성을 떼어 더 가벼워짐.
- 0.3(2024년 9월) — 내부를 Pydantic 2로 바꾸고, Python 3.8 지원을 끝냄.
버전은 자주 바뀌어도 전체 구조는 거의 그대로다. 새 버전이 나오면 바뀐 점만 빠르게 보면 된다.
왜 굳이 랭체인인가 (1.3)
OpenAI만으로도 앱은 만든다. 그래도 랭체인을 쓰는 이유 4가지.
| 장점 | 한 줄 뜻 |
|---|---|
| 모듈성 | 기능을 블록으로 끼웠다 뺐다 한다 |
| 통합 용이성 | OpenAI → Gemini 교체를 설정만 바꿔 한다 |
| 확장 기능 | LCEL·러너블로 복잡한 흐름을 짧게 쓴다 |
| 커뮤니티 | 예제·문서가 많고 자주 업데이트된다 |
무엇에 쓰나 (1.4) — RAG와 질의응답, 구조화 출력 뽑기, 챗봇, 도구 사용·에이전트. 이 책의 각 장이 이 쓰임을 하나씩 다룬다.
한 걸음 더 ▸ (지금 몰라도 됨) — 패키지가 많아 보여도, 처음엔
langchain·langchain-core·langchain-openai세 개면 충분하다. 나머지는 필요할 때 하나씩 꺼내 쓰면 된다.
개념 2 — 모델은 직접 안 만들고, 감싸서 갈아 끼운다
망가지는 장면
OpenAI 호출법으로 코드를 가득 채웠다.
모델을 바꾸려니 호출법이 달라, 곳곳을 다 뜯어고친다.
도입부의 바로 그 고생이다.
일상비유
여러 나라 플러그를 한 모양으로 맞춰 주는 멀티 어댑터.
벽 콘센트가 무엇이든, 어댑터 너머는 늘 같은 모양이라 기기를 그대로 꽂는다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 어댑터 없이 직접 꽂기 | client.chat.completions.create(model=...) |
모델 바꾸면 곳곳 수정 |
| 어댑터로 감싸 꽂기 | ChatOpenAI(model="gpt-4o") |
교체는 클래스 한 줄만 |
한 문장 정의 — 랭체인은 LLM을 직접 만들지 않고, OpenAI·Cohere·Hugging Face 같은 여러 제공자를 표준 인터페이스로 감싸 똑같이 다루게 해 준다.
예시 폭격 — 직접 호출 vs 랭체인
(worked) 잘못된 결(특정 모델에 딱 붙음):
# OpenAI 직접 — 모델 교체 시 호출부를 다 고쳐야
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
import openai
# 답변을 생성할 LLM 클라이언트나 모델 설정을 준비합니다.
client = openai.OpenAI()
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
client.chat.completions.create(
# 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
model="gpt-4o-mini",
# 모델에게 보낼 대화 메시지 목록을 지정합니다.
messages=[{"role": "user", "content": "안녕하세요!"}]
)
(worked) 올바른 결(블록 하나만 갈면 됨):
# 랭체인 — 모델은 클래스 한 줄. 교체도 한 줄.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import ChatOpenAI
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import ChatPromptTemplate
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import StrOutputParser
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_template("주제 {topic}에 대해 짧은 설명을 해주세요.")
# 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
model = ChatOpenAI(model="gpt-4o")
# 미스트랄로? 윗줄만 ChatMistralAI(...) 로.
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
chain = prompt | model | StrOutputParser()
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
chain.invoke({"topic": "더블딥"})
(부분 완성) 두 방식을 표로 비교해 빈칸을 채워 보자.
| 항목 | OpenAI 직접 | 랭체인 |
|---|---|---|
| 구조 | 단순 호출 | 모듈화된 체인 |
| 모델 전환 | 코드 수정 필요 | ____ |
| 적합 | 간단한 작업 | 복잡·확장 앱 |
→ 빈칸: 클래스만 교체.
(독립 적용) "간단한 스크립트 하나, 모델 바꿀 일 없음" — 이럴 땐? → 굳이 랭체인 없이 OpenAI 직접 호출도 충분하다. 도구는 일에 맞춰 고른다.
모델의 손잡이들 (파라미터 2.2)
같은 모델도 손잡이를 돌려 답의 결을 바꾼다.
| 손잡이 | 무엇을 조절 |
|---|---|
| temperature(0~1) | 낮으면 또박또박 일관, 높으면 톡톡 튀게 |
| max_tokens | 답 길이의 상한 |
| top_p | 상위 확률 단어만 고르기 |
| frequency / presence penalty | 반복 줄이기 / 새 단어 장려 |
| stop sequences | 특정 글자 만나면 생성 멈추기 |
# 온도 낮춤 = 매번 비슷한 답, 길이는 100조각까지
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import OpenAI
# 답변을 생성할 LLM 클라이언트나 모델 설정을 준비합니다.
llm = OpenAI(temperature=0.2, max_tokens=100)
어떤 모델을 고르나 (2.3) — 제공자마다 문맥 크기(한 번에 읽는 글 양)·백만 토큰당 입출력 비용·한국어 성능이 다르다.
한국어 성능은 LogicKor 같은 대시보드로 비교한다.
비용과 모델은 수시로 바뀌므로, 쓸 때 공식 가격표를 꼭 확인한다.
한 걸음 더 ▸ — 책의
gpt-4o·Claude 3.5·Gemini는 2025년 기준 예시다. 실제로 쓸 땐 공식 모델 목록을 본다: OpenAI·Anthropic·Google. (검증 2026-05-21)
개념 3 — LCEL: 블록을 |로 한 줄에 줄 세운다
망가지는 장면
프롬프트 만들고, 모델 부르고, 답을 정리하고… 단계가 많다.
이걸 하나하나 손으로 이으면, 중간에 데이터 모양이 안 맞아 자꾸 어긋난다.
일상비유
컨베이어 벨트. 재료를 올리면 자르고 → 굽고 → 포장이 차례로 흐른다.
각 칸은 앞 칸이 넘긴 걸 받아 처리하고 다음 칸에 넘긴다. 사람이 일일이 들고 옮기지 않는다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 벨트로 칸 잇기 | chain = prompt \| model \| parser |
앞 칸 출력 모양이 다음 칸 입력과 안 맞으면 막힘 |
| 벨트에 재료 올리기 | chain.invoke({"topic": "더블딥"}) |
입력 키 이름 틀리면 빈칸 안 채워짐 |
한 문장 정의 — LCEL(랭체인 표현 언어)은 러너블 블록을 파이프(|)로 이어, "무엇을 할지"를 선언적으로 한 줄에 쓰는 방식이다.
선언적이란? — "어떻게 하나하나 하라"가 아니라 "무엇을 할지"만 적는 것이다.
벨트 설계도만 그려 두면, 재료가 알아서 흐르는 식이다.
모든 블록이 공유하는 공통 버튼 (러너블 표준 메서드 3.1)
프롬프트·모델·파서가 다 러너블이라, 같은 버튼으로 부른다.
| 동기 | 비동기 | 무슨 일 |
|---|---|---|
invoke() |
ainvoke() |
입력 하나 처리 |
batch() |
abatch() |
여러 입력 한꺼번에 |
stream() |
astream() |
답을 토막토막 실시간으로 |
# 같은 체인, 부르는 버튼만 다름
chain.invoke({"topic": "더블딥"}) # 하나
chain.batch([{"topic": "더블딥"}, {"topic": "인플레이션"}]) # 여럿
for token in chain.stream({"topic": "더블딥"}): # 토막 실시간
# 중간 결과를 눈으로 확인해 흐름이 맞는지 봅니다.
print(token, end="", flush=True)
참고 —
stream()은 답이 다 만들어지기 전에 토막을 흘려보낸다.flush=True는 "화면에 바로바로 보여라"라는 뜻이다. 채팅창에 글자가 또르륵 찍히는 그 느낌이다.
예시 폭격 — 블록 잇는 여러 방법 (3.2)
(worked) 파이프 연산자 — 가장 흔한 방법:
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import ChatOpenAI
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import ChatPromptTemplate
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import StrOutputParser
# 사용할 모델 이름을 지정해 어떤 LLM으로 답할지 정합니다.
model = ChatOpenAI(model="gpt-4o-mini")
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_template("주제 {topic}에 대해 짧은 설명을 해주세요.")
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = StrOutputParser()
chain = prompt | model | parser # 이 | 가 "이어 붙여라"
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
chain.invoke({"topic": "더블딥"})
참고 —
|는 어떻게 작동하나 — 파이썬은 원래|로 이런 연결을 안 해 준다. 랭체인이__or__라는 특별 약속을 덧씌워서(오버로딩) "|를 만나면 블록을 이어라"로 바꿔 둔 것이다. 지금은 "랭체인이|에 새 뜻을 줬다"만 알면 된다.
(worked) .pipe() 메서드 — 같은 일을 메서드로:
# prompt | model | parser 와 같은 결과
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
chain = prompt.pipe(model).pipe(parser)
(worked) 체인 뒤에 체인 잇기 — 설명을 만들고 영어로 번역까지:
# {"answer": chain} 으로 앞 결과를 받아 → 번역 프롬프트로 넘김
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
analysis_prompt = ChatPromptTemplate.from_template("이 대답을 영어로 번역해 주세요: {answer}")
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
composed = {"answer": chain} | analysis_prompt | model | StrOutputParser()
composed.invoke({"topic": "더블딥"}) # 더블딥 설명을 영어로 받음
(부분 완성) 중간에 손수 만든 함수도 블록으로 끼울 수 있다. 함수는 러너블로 자동 변환된다.
# chain 결과를 {"answer": ...} 모양으로 바꿔 다음에 넘기는 람다
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
composed = chain | (lambda x: {"answer": x}) | analysis_prompt | model | StrOutputParser()
빈칸: 단, 이렇게 람다를 끼우면 ____ 작업과 호환이 안 될 수 있어 주의한다. → 답: 스트리밍.
(독립 적용) "같은 주제를 한국어·영어로 동시에 받고 싶다." → RunnableParallel로 두 체인을 나란히 돌린다.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.runnables import RunnableParallel
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
kor = ChatPromptTemplate.from_template("{topic}에 대해 짧은 설명을 해주세요.") | model | parser
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
eng = ChatPromptTemplate.from_template("{topic}에 대해 짧게 영어로 설명을 해주세요.") | model | parser
# `parallel`에 중간 결과를 담아 다음 줄에서 재사용합니다.
parallel = RunnableParallel(kor=kor, eng=eng)
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
result = parallel.invoke({"topic": "더블딥"})
# result["kor"], result["eng"] 로 둘 다 받음
미니 시나리오 — 사용자가 "응답이 너무 느려요. 다 나올 때까지 빈 화면이에요" 한다.
→ invoke 대신 stream으로 바꿔, 만들어지는 토막을 바로바로 보여 준다. 체감 속도가 확 산다.
개념 4 — 프롬프트 템플릿: 빈칸 양식
망가지는 장면
주제마다 지시문을 통째로 새로 쓴다. "더블딥 설명해 줘", "인플레이션 설명해 줘"…
같은 문장을 매번 복사·수정한다. 실수도 늘고 지저분하다.
일상비유
빈칸 있는 양식 편지. "주제 ____에 대해 설명해 줘"를 만들어 두고, 빈칸에 단어만 갈아 끼운다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 빈칸 양식 만들기 | ChatPromptTemplate.from_template("주제 {topic}…") |
빈칸 이름과 입력 키가 다르면 안 채워짐 |
| 빈칸에 값 끼우기 | prompt.invoke({"topic": "주식"}) |
딕셔너리로 줘야 함 |
한 문장 정의 — 프롬프트 템플릿은 사용자 입력을 모델용 지시문으로 바꾸는 빈칸 양식이며, {변수} 자리에 값을 채워 완성한다. 입력은 딕셔너리로 받는다.
세 가지 양식
| 양식 | 언제 |
|---|---|
PromptTemplate |
단순한 한 줄 문자열 지시 |
ChatPromptTemplate |
system/user/ai 역할이 있는 대화형 |
MessagesPlaceholder |
대화 이력처럼 목록을 끼울 빈칸 |
예시 폭격 — 양식 만들기
(worked) 문자열 양식:
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import PromptTemplate
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = PromptTemplate.from_template("주제 {topic}에 대해 짧은 조언을 해주세요.")
prompt.invoke({"topic": "투자"}) # → "주제 투자에 대해 …" 완성
(worked) 챗 양식 — 역할을 나눠 줌:
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import ChatPromptTemplate
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_messages([
("system", "당신은 유능한 금융 조언가입니다."), # AI 역할 고정
("user", "주제 {topic}에 대해 조언을 해주세요"), # 사용자 요청
])
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
prompt.invoke({"topic": "주식"})
(부분 완성) 대화 목록을 통째로 끼울 빈칸:
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.messages import HumanMessage
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_messages([
# 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
("system", "당신은 유능한 금융 조언가입니다."),
MessagesPlaceholder("msgs"), # 또는 ("placeholder", "{msgs}")
])
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
prompt.invoke({"msgs": [HumanMessage(content="안녕하세요!")]})
빈칸: MessagesPlaceholder("msgs") 대신 더 짧게 쓰면 ("placeholder", "____") 이다. → 답: {msgs}.
(독립 적용) "예시를 몇 개 보여 주면 모델이 더 잘한다던데?" → 그게 퓨샷이다(아래 4.1).
퓨샷 프롬프트 (4.1)
예시를 보여 주는 개수로 이름이 갈린다.
- 0개 = 제로샷
- 1개 = 원샷
- n개 = 퓨샷(n샷)
예시를 한꺼번에 넣어 주는 게 FewShotPromptTemplate이다.
(worked) 잘못된 결 — 예시를 전부 욱여넣음(토큰 낭비):
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.prompts import FewShotPromptTemplate
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = FewShotPromptTemplate(
examples=examples, # 예시 전부
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
example_prompt=example_prompt,
# 여러 값을 이름표가 붙은 구조로 묶어 전달합니다.
suffix="질문: {input}",
# 프롬프트가 바깥에서 받을 입력 이름을 적습니다.
input_variables=["input"],
)
(worked) 올바른 결 — 예제 선택기로 비슷한 예시만 골라 넣음(토큰 절약):
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_chroma import Chroma
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_openai import OpenAIEmbeddings
# `selector`에 중간 결과를 담아 다음 줄에서 재사용합니다.
selector = SemanticSimilarityExampleSelector.from_examples(
# 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
examples,
OpenAIEmbeddings(api_key=api_key), # 예시를 숫자로 바꿔
Chroma, # 창고에 넣고 비교
k=1, # 가장 비슷한 1개만 고름
)
미니 시나리오 — 예시가 50개라 매번 다 넣으니 비용이 폭발한다.
→ 예제 선택기에 k=1을 줘, 질문과 가장 가까운 한 개만 골라 넣는다. 비용이 뚝 떨어진다.
프롬프트 허브 (4.2)
남이 잘 만든 프롬프트를 가져다 쓰는 중앙 창고다.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain import hub
prompt = hub.pull("hardkothari/prompt-maker") # 최신 버전 가져오기
prompt = hub.pull("hardkothari/prompt-maker:c5db8eee") # 특정 버전 고정
:c5db8eee 처럼 버전을 콕 집으면, 나중에 원본이 바뀌어도 내가 쓰던 그 버전을 그대로 쓴다.
개념 5 — 출력 파서: 줄글을 정리된 모양으로
망가지는 장면
모델이 "부동산 투자는 장기적으로 안정적이며…" 하고 줄글로 답했다.
이걸 프로그램에서 "질문 부분"과 "답변 부분"으로 나눠 쓰려는데, 줄글이라 칼같이 안 잘린다.
일상비유
손님의 말 주문을 주방 주문표 칸에 옮겨 적는 점원.
말은 자유롭지만, 표는 '메뉴' 칸 '수량' 칸이 정해져 있어 주방이 헷갈리지 않는다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 줄글을 깔끔한 문자열로 | StrOutputParser() |
구조가 필요하면 부족함 |
| 줄글을 칸 있는 표(JSON)로 | JsonOutputParser() |
모델이 형식 안 지키면 오류 |
한 문장 정의 — 출력 파서는 모델의 자유 텍스트를 JSON·CSV·검증된 데이터 같은 구조화된 모양으로 바꿔, 프로그램이 곧장 쓰게 해 준다.
참고 — 요즘 모델은 함수·도구 호출을 직접 지원한다. 그쪽이 가능하면 출력 파서 대신 그걸 권한다. 출력 파서는 그게 안 되거나 단순한 경우에 쓴다.
출력 파서가 가진 세 버튼 (5.1)
| 버튼 | 무슨 일 |
|---|---|
get_format_instructions() |
"이런 형식으로 답해" 지침을 모델에 줄 문구 만들기 |
parse() |
모델 답(줄글·JSON 문자열) → 파이썬 데이터로 변환 |
parse_with_prompt() |
질문까지 함께 받아, 형식 틀렸을 때 고쳐 다시 시도 |
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import JsonOutputParser
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = JsonOutputParser()
print(parser.get_format_instructions()) # 모델에 줄 형식 지침
# 줄글 JSON을 파이썬 딕셔너리로
parser.parse('{"이름": "김철수", "나이": 30}') # → {"이름": "김철수", "나이": 30}
예시 폭격 — 파서 종류별로
(worked) PydanticOutputParser — 칸을 정해 두고 검증까지:
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import PydanticOutputParser
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from pydantic import BaseModel, Field, model_validator
# 관련 데이터와 동작을 하나의 이름 아래 묶는 클래스입니다.
class FinancialAdvice(BaseModel):
# `setup: str`에 중간 결과를 담아 다음 줄에서 재사용합니다.
setup: str = Field(description="금융 조언 상황 질문")
# `advice: str`에 중간 결과를 담아 다음 줄에서 재사용합니다.
advice: str = Field(description="질문에 대한 금융 답변")
# 아래 함수나 클래스를 프레임워크가 특별한 용도로 인식하게 표시합니다.
@model_validator(mode="before")
# 아래 함수나 클래스를 프레임워크가 특별한 용도로 인식하게 표시합니다.
@classmethod
# 반복해서 쓸 처리 흐름에 이름을 붙인 함수입니다.
def must_end_with_question(cls, values):
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if not values.get("setup", "").endswith("?"):
raise ValueError("질문은 '?'로 끝나야 합니다.") # 형식 어기면 막음
# 계산하거나 처리한 결과를 이 함수를 부른 쪽으로 돌려줍니다.
return values
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = PydanticOutputParser(pydantic_object=FinancialAdvice)
(worked) SimpleJsonOutputParser — 가벼운 JSON, 스트리밍 지원:
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain.output_parsers.json import SimpleJsonOutputParser
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
json_parser = SimpleJsonOutputParser()
# 값이 한 글자씩 차오르는 걸 실시간으로 볼 수 있음
# {} → {"answer": "비"} → {"answer": "비트"} → …
(부분 완성) JsonOutputParser — JSON 특화. Pydantic 스키마도 결합:
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.output_parsers import JsonOutputParser
# 모델 응답을 다음 단계에서 쓰기 쉬운 형태로 정리합니다.
parser = JsonOutputParser(pydantic_object=FinancialAdvice)
# 결과는 ____ 모양으로 나온다: {"setup": "...", "advice": "..."}
빈칸: 딕셔너리.
(독립 적용) 파서가 너무 많다. 표로 골라 보자.
| 파서 | 스트리밍 | 검증 | 언제 |
|---|---|---|---|
StrOutputParser |
됨 | 없음 | 그냥 문자열 |
SimpleJsonOutputParser |
됨 | 없음 | 가벼운 JSON |
JsonOutputParser |
됨 | 선택 | JSON + 스키마 |
PydanticOutputParser |
제한적 | 있음 | 형식 검증이 꼭 필요할 때 |
미니 시나리오 — 모델이 가끔 ? 없이 질문을 만든다.
→ PydanticOutputParser에 검증 규칙(?로 끝나야 함)을 넣어, 형식 어긴 답을 걸러 낸다.
개념 6 — 메모리: 챗봇이 앞 대화를 기억한다
망가지는 장면
챗봇에게 "저축 어떻게 늘려요?" 묻고 답을 받았다.
이어 "방금 뭐라고 했죠?" 하니, "무슨 말씀이신지 모르겠어요" 한다.
방금 한 말을 전혀 기억 못 한다. 매 질문이 첫 만남이다.
일상비유
통화 중 메모지. 앞에 한 말을 적어 두고, "아까 뭐랬죠?" 하면 메모지를 보고 답한다.
메모지가 없으면 매번 처음 통화하는 사람처럼 군다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 메모지를 같이 건넴 | ("placeholder", "{messages}") |
안 건네면 앞 대화 까먹음 |
| 메모지가 너무 길어짐 | 트리밍·요약으로 줄임 | 안 줄이면 느려지고 비싸짐 |
한 문장 정의 — 메모리는 챗봇이 이전 대화를 기억해 맥락을 이어 가게 하는 장치이며, 그냥 넘기기 → 저장 → 자동 관리 → 줄이기·요약, 점점 더 손이 덜 가게 발전한다.
예시 폭격 — 점점 자동으로 (네 단계)
(worked) 1단계 — 그냥 넘기기(6.1): 앞 대화를 손수 프롬프트 빈칸에 넣는다.
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
prompt = ChatPromptTemplate.from_messages([
# 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
("system", "당신은 금융 상담사입니다."),
("placeholder", "{messages}"), # 여기에 대화 이력을 통째로
])
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
chain = prompt | chat
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
chain.invoke({"messages": [
# 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
("human", "저축을 늘리려면?"),
# 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
("ai", "매달 자동이체로 저축하세요."),
("human", "방금 뭐라고 했죠?"), # 앞 두 줄을 같이 줘서 기억함
]})
(worked) 2단계 — ChatMessageHistory(6.2): 대화를 차곡차곡 저장한다.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_community.chat_message_histories import ChatMessageHistory
# `history`에 중간 결과를 담아 다음 줄에서 재사용합니다.
history = ChatMessageHistory()
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
history.add_user_message("저축을 늘리려면?")
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
history.add_ai_message("매달 자동이체로 저축하세요.")
# 다음 질문 때 history.messages 를 넘기면 됨
(부분 완성) 3단계 — RunnableWithMessageHistory(6.3): 체인을 감싸 자동 저장·불러오기.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.runnables.history import RunnableWithMessageHistory
# 프롬프트·모델·출력 처리를 하나의 실행 흐름으로 이어 붙입니다.
chain_auto = RunnableWithMessageHistory(
# 프롬프트·모델·출력 처리를 하나의 실행 흐름으로 이어 붙입니다.
chain,
# 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
lambda session_id: history,
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
input_messages_key="input",
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
history_messages_key="chat_history",
)
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
chain_auto.invoke(
# 여러 값을 이름표가 붙은 구조로 묶어 전달합니다.
{"input": "저축을 늘리려면?"},
{"configurable": {"session_id": "____"}}, # 대화방을 구분하는 표
)
빈칸: session_id에는 대화방마다 다른 이름을 준다(예: "session_1"). 같은 이름이면 같은 메모지를 공유한다.
(독립 적용) 4단계 — 대화가 100턴 넘게 길어지면? 두 가지로 줄인다.
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
from langchain_core.messages import trim_messages
# 트리밍 — 최근 2개만 남기고 잘라 냄
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
trimmer = trim_messages(strategy="last", max_tokens=2, token_counter=len)
# 요약 — 옛 대화를 압축해 핵심만 남김 (요약 프롬프트로 모델이 줄임)
미니 시나리오 — 상담 챗봇이 너무 느리고 토큰비가 많이 나온다.
→ 트리밍은 빠르지만 오래된 맥락을 잃는다. 요약은 토큰을 크게 아끼지만 중요 정보가 빠질 위험이 있다.
초반 맥락이 중요하면 요약을, 속도가 중요하면 트리밍을 고른다. 둘은 트레이드오프다.
한 걸음 더 ▸ (지금 몰라도 됨) — 요즘 권장은 대화 상태를
langgraph로 관리하는 쪽으로 옮겨 가고 있다. 개념은 이 절로 충분하다. 신규 코드를 짤 땐 공식 문서의 최신 권장을 확인하면 된다.
단순 규칙 (헷갈리면 이대로)
- 모델은 직접 호출하지 말고 체인으로:
prompt | model | parser. 나중에 교체가 쉽다. - 일관된 답이 필요하면
temperature=0, 창의적이면 높인다. - 구조화된 답이 필요하면 출력 파서(또는 가능하면 함수·도구 호출).
- 챗봇엔
RunnableWithMessageHistory+session_id로 대화방별 자동 기억. - 대화가 길어지면 트리밍이나 요약으로 줄인다.
- 모델·가격은 공식 페이지에서 확인 — 책 표기는 2025년 예시다.
정리 (핵심 3줄)
랭체인은 모델·프롬프트·파서·메모리를 같은 규격의 블록(러너블)으로 감싸, |(LCEL)로 한 줄에 잇게 해 주는 부품 상자다.
덕분에 모델 교체는 블록 하나 갈아 끼우기로, 복잡한 흐름은 짧은 한 줄로 줄어든다.
이 여섯 블록(생태계·모델·LCEL·프롬프트·파서·메모리)이 다음에 만들 RAG의 바탕이 된다.
더 해보기
- 책 공식 실습 노트북(코랩) — Ch01. Langchain Basics — LCEL·LLM·프롬프트·출력 파서·메모리 노트북. 브라우저에서 바로 LCEL 코랩 열기.
- 랭체인 공식 문서 — python.langchain.com/docs (전체 길잡이) · LCEL·러너블 · 프롬프트 템플릿 · 출력 파서 · 예제 선택기.
다음 2장에서는 이 블록들을 진짜로 끼워, 문서를 읽어 답하는 첫 RAG를 직접 만든다. (지금 6블록만 손에 있으면 충분합니다.)
연습문제
- 설명.
랭체인이라는 레고 상자의 핵심을 처음 듣는 사람에게 한 문장으로 설명하라. - 구분. 두 개념(
LCEL 체인,직접 호출)을 실제 예시 하나로 구분하라. - 적용. 내 프로젝트나 학습 노트에서 이 장의 개념을 적용해 작게 개선할 지점을 하나 고르라.
부록 A. 쉬운 용어 사전
| 용어 | 아주 쉬운 뜻 | 이 장에서 나온 위치 |
|---|---|---|
| LCEL 체인 | 랭체인 부품을 |로 이어 하나의 처리 흐름처럼 만든 것. |
부록 B와 본문 예시 |
| 직접 호출 | 체인이나 공통 규격 없이 모델 API나 함수를 바로 부르는 방식. | 부록 B와 본문 예시 |
| 프롬프트 템플릿 | 매번 달라지는 입력을 빈칸에 넣어 같은 질문 양식을 만드는 틀. | 부록 B와 본문 예시 |
| 출력 파서 | 모델 답을 표, JSON, 목록처럼 쓰기 쉬운 모양으로 정리하는 부품. | 부록 B와 본문 예시 |
부록 B. 헷갈리는 개념 비교표
| A | B | 구분 포인트 |
|---|---|---|
| LCEL 체인 | 직접 호출 | 체인은 부품을 바꾸기 쉽고, 직접 호출은 빠르지만 흐름이 흩어진다. |
| 프롬프트 템플릿 | 출력 파서 | 앞은 질문 양식, 뒤는 답변 모양 정리 담당이다. |
부록 C. 더 읽을 자료
- 이 장의
더 해보기섹션 — 이미 모아 둔 공식 문서나 실습 링크가 있으면 여기서 먼저 확인한다. - 같은 책의
0장 한눈에 보기— 용어가 막히면 0장의 용어집과 개념 척추로 돌아간다. - 원본 딥다이브판 같은 장 — 입문판을 읽고 큰 흐름이 잡힌 뒤 세부 논리를 더 깊게 확인한다.
- 이 장의
flashcards.json— 읽은 직후 질문만 보고 답을 떠올리는 회상 연습에 쓴다.
부록 D. 연습문제 풀이
- 설명 예시.
랭체인이라는 레고 상자는 RAG에서 자료를 더 잘 찾고, 근거를 더 안전하게 붙이고, 답변 흐름을 더 다루기 쉽게 만드는 방법을 보는 장이다. 중요한 것은 용어를 외우는 것이 아니라, 이 개념이 어떤 입력·부품·결정에 영향을 주는지 말로 풀어 보는 것이다. - 구분 예시. 두 개념(
LCEL 체인,직접 호출)의 차이는 이렇게 잡으면 된다. 체인은 부품을 바꾸기 쉽고, 직접 호출은 빠르지만 흐름이 흩어진다. 실제 사례를 볼 때는 목적, 입력, 실패했을 때의 증상을 따로 적어 보면 헷갈리지 않는다. - 적용 예시. 가장 작은 개선부터 고른다. 예를 들어 이름을 더 분명히 하거나, 평가 기준을 한 줄 추가하거나, 직접 알 필요 없는 내부 정보를 감추는 식으로 시작한다. 한 번에 크게 갈아엎는 것보다 작은 변경 하나를 확인하며 진행하는 쪽이 입문 단계에 맞다.
클릭하거나 Space를 눌러 뒤집기